﻿using System;

using System.Collections.Generic;
using System.Text;
using Silvermoon.Animations;
using Silvermoon.Windows;
using System.Drawing;
using System.Diagnostics;
using Silvermoon.Controls.Base;
using Silvermoon.UI;

namespace Silvermoon.Controls
{
    /// <summary>
    /// Controls that implements base functionality for vertical scrolling.
    /// This also includes management of a scrollbar.
    /// </summary>
    public abstract class ScrollViewer : CompositeControlBase
    {
        #region const

        private static readonly double Friction = Math.Log(0.2d) / 1000d;

        public const int ScrollbarWidth = VScrollBar.ScrollbarWidth;

        #endregion
        #region statics

        static ScrollViewer()
        {
            VisualStateManager manager = new VisualStateManager(
                new VisualState("ScrollbarHidden",
                    new IntTransition { Duration = 2000, EasingFunction = SineEase.InOut, Property = ScrollViewer.ScrollbarOpacityProperty, To = 0, Tracking = TrackMode.Keep }
                    ),
                new VisualState("ScrollbarVisible",
                    new IntTransition { Duration = 0, EasingFunction = SineEase.InOut, Property = ScrollViewer.ScrollbarOpacityProperty, To = 200, Tracking = TrackMode.Keep }
                    )
              );

            VisualStateManager.RegisterClassManager(typeof(ScrollViewer), manager);
        }


        #endregion
        #region fields

        protected const int FLICKMODE = 1;
        protected const int SCROLLBARVISIBLE = 2;
        protected const int SCROLLING = 4;
        protected const int PAN = 8;

        private PhysicsMode physicsMode = PhysicsMode.Friction;

        private float decelleration = 1f;
        private VScrollBar scrollBar;
        protected int yOffset;

        // to be used internally as storage for bool properties.
        protected int bitmask;

        #endregion
        #region ctor

        public ScrollViewer()
            : base()
        {
            BounceDuration = 200;
            GotoDuration = 300;
            Decelleration = 1f;
            ControlFlags = ControlFlags | ControlFlags.CanPan;
        }

        protected override void Dispose(bool dispose)
        {
            DisposeObject(scrollBar);
            base.Dispose(dispose);
        }

        #endregion
        #region props


        /// <summary>
        /// Gets or sets the physics mode for slowing down scrolling.
        /// </summary>
        public PhysicsMode PhysicsMode
        {
            get { return physicsMode; }
            set { physicsMode = value; }
        }

        /// <summary>
        /// Gets or sets the duration in ms when the offset bounces to the start or end location. the default value is 200 ms.
        /// </summary>
        public int BounceDuration { get; set; }

        /// <summary>
        /// Gets or sets the duration in ms when the offset is animated to an new value. the default value is 300 ms.
        /// </summary>
        public int GotoDuration { get; set; }

        /// <summary>
        /// Gets or sets the deceleration value when the content is scrolled due to a flick event.
        /// The default value is 1. The higher the value, the sooner the content stops to scroll. Values equal or less than 0 are not allowed.
        /// </summary>
        public float Decelleration
        {
            get { return decelleration; }
            set
            {
                if (decelleration <= 0f) throw new ArithmeticException("Decelleration must be larger than 0.");
                decelleration = value;
            }
        }

        protected VScrollBar ScrollBar
        {
            get
            {
                if (scrollBar == null) SetScrollbar();
                return scrollBar;
            }
        }

        /// <summary>
        /// Gets or sets the color for the scrollbar.
        /// </summary>
        public Color ScrollbarColor
        {
            get { return ScrollBar.Color; }
            set
            {
                if (ScrollBar.Color != value)
                {
                    ScrollBar.Color = value;
                    Invalidate(InvalidationFlag.Color);
                }
            }
        }


        /// <summary>
        /// Gets or sets the offset of the first pixel that is displayed at top.
        /// </summary>
        public int YOffset
        {
            get { return yOffset; }
            set
            {
                if (yOffset != value)
                {
                    int oldValue = yOffset;
                    yOffset = value;
                    OnYOffsetChanged(oldValue);
                }
            }
        }

        /// <summary>
        /// Gets the client bounds of the control which are not overlapped by parts of a toolbar or keyboard.
        /// </summary>
        public Rectangle VisibleBounds
        {
            get
            {
                Rectangle bounds = ClientBounds;
                Window window = Window;
                if (window != null)
                {
                    Rectangle windowBounds = window.VisibleRect;
                    Point p = new Point(windowBounds.Right, windowBounds.Bottom);
                    p = PointToClient(p);
                    int h = p.Y - bounds.Top;
                    if (bounds.Height > h) bounds.Height = h;
                }
                return bounds;
            }
        }

        protected virtual bool IsScrollbarAvailable
        {
            get { return scrollBar != null && scrollBar.Visible; }
        }

        /// <summary>
        /// Gets the height of the content. If this value is larger than the control's height, a scrollbar becomes available.
        /// </summary>
        public abstract int VirtualHeight { get; }

        /// <summary>
        /// Gets whether the scrollbar is currently visible
        /// </summary>
        public bool IsScrollbarVisible
        {
            get { return (bitmask & SCROLLBARVISIBLE) != 0; }
            set
            {
                if (IsScrollbarVisible != value)
                {
                    bitmask = value ? bitmask | SCROLLBARVISIBLE : bitmask & ~SCROLLBARVISIBLE;
                    OnScrollbarVisibleChanged();
                }
            }
        }

        protected bool IsPanning
        {
            get { return (bitmask & PAN) != 0; }
            set
            {
                if (IsPanning != value)
                {
                    bitmask = value ? bitmask | PAN : bitmask & ~PAN;
                    OnPanningChanged();
                }
            }
        }


        private bool FlickMode
        {
            get { return (bitmask & FLICKMODE) != 0; }
            set
            {
                bitmask = value ? bitmask | FLICKMODE : bitmask & ~FLICKMODE;
            }
        }

        public bool IsScrolling
        {
            get { return (bitmask & SCROLLING) != 0; }
            set
            {
                if (IsScrolling != value)
                {
                    bitmask = value ? bitmask | SCROLLING : bitmask & ~SCROLLING;
                    OnScrollingChanged();
                }
            }
        }

        /// <summary>
        /// Gets or sets the opacity for the scrollbar.
        /// </summary>
        protected int ScrollbarOpacity
        {
            get { return scrollBar != null ? scrollBar.Opacity : 0; }
            set
            {
                ScrollBar.Visible = value > 0;
                scrollBar.Opacity = value;
            }
        }

        #endregion
        #region methods

        protected bool IsScrollbar(Control c)
        {
            return scrollBar == c;
        }

        protected override Point GetOffset()
        {
            return new Point(0, yOffset);
        }

        public override void OnLostFocus()
        {
            YOffsetProperty.StopAnimation(this);
            EnsureRestState(true);
            base.OnLostFocus();
        }

        protected virtual void OnPanningChanged()
        {
            DetermineScrollbarVisibility();
        }

        protected virtual void DetermineScrollbarVisibility()
        {
            IsScrollbarVisible = IsPanning | IsScrolling;
        }

        protected virtual void OnScrollbarVisibleChanged()
        {
            bool visible = IsScrollbarVisible;
            SetState(visible ? "ScrollbarVisible" : "ScrollbarHidden");
            Invalidate(InvalidationFlag.Layout);
            if (!visible) EnsureRestState(true);
        }

        protected virtual void OnScrollingChanged()
        {
            DetermineScrollbarVisibility();
            RaiseEvent(ScrollingChangedEvent);
        }

        protected override void OnLayout()
        {
            if (IsScrollbarAvailable)
            {
                if (CheckFlag(InvalidationFlag.Size | InvalidationFlag.Layout))
                {
                    SetScrollbar();
                }
                scrollBar.Value = yOffset;
            }
            base.OnLayout();
        }

        private void SetScrollbar()
        {
            if (scrollBar == null)
            {
                scrollBar = new VScrollBar { Opacity = 0, Visible = false };
                scrollBar.ValueChanged += new EventHandler(OnScrollbarValueChanged);
                VisibleControls.AddLast(scrollBar);
            }

            scrollBar.Bounds = new Rectangle(Width - VScrollBar.ScrollbarWidth, 0, VScrollBar.ScrollbarWidth, Height);
            scrollBar.Maximum = VirtualHeight;
            scrollBar.LargeChange = Height;
        }

        private void OnScrollbarValueChanged(object sender, EventArgs e)
        {
            YOffset = scrollBar.Value;
        }

        internal protected override void OnBeginPan(PanEventArgs e)
        {
            //HideKeyboard();
            PanInternal(e);
            base.OnBeginPan(e);
        }

        private void HideKeyboard()
        {
            Window w = Window;
            if (w != null) w.IsKeyboardVisible = false;
        }

        protected virtual void PanInternal(PanEventArgs e)
        {
            if (!e.Handled)
            {
                if (VirtualHeight > Height)
                {
                    if (e.VerticalMotion)
                    {
                        e.Handled = true;
                        e.Value = new Point(0, yOffset);
                        IsPanning = true;
                    }
                }
            }
        }

        /// <summary>
        /// Occurs when no further OnPan events will occur.
        /// This method is also the place to undo preparations for swiping, e.g. hiding a scrollbar.
        /// </summary>
        /// <param name="e"></param>
        protected internal override void OnEndPan(EventArgs e)
        {
            IsPanning = false;
            base.OnEndPan(e);
        }


        /// <summary>
        /// Animates the YOffset property to decelerate to a target value where target value and duration depends on the velocity value in vertical direction.
        /// </summary>
        /// <param name="e">The <see cref="Silvermoon.Controls.TouchEventArgs"/> instance containing the event data.</param>
        protected internal override void OnFlick(TouchEventArgs e)
        {
            if (!e.Handled && e.VerticalMotion)
            {
                e.Handled = true;
                e.Cancel = true;

                // dont' flick outside the valid range:
                if (!(e.Direction == PanDirection.Up && yOffset < 0) && !(e.Direction == PanDirection.Down && yOffset >= VirtualHeight))
                {
                    ScrollByVelocity(e.VerticalVelocity);
                }
            }
            base.OnFlick(e);
        }

        /// <summary>
        /// Animates the YOffset property depending on the velocity (v) of the flick event.
        /// </summary>
        /// <param name="v">Velocity from flick event.</param>
        /// <remarks>
        /// Here is some math that explains the calculation to understand:
        /// The animation is acually a decelleration with the formula s(t) = v*t+0.5*a*t² where v is the velocity and a a constant deceleration.
        /// The duration of the animation must end when s(t)' =0. This is when vt=at² => t = v/a;
        /// Therefore the distance s which is reached at this time is s(max) = v*(v/a)-0.5*(v/a)² which is s(max) = 0.5 * v²/a.
        /// Since s(t) is actually a transformed f(x)=x², a QuadraticEase is used for transitioning.
        /// Note that durationMultiplicator is actually 1/a while offsetMultiplicator is 0.5*(1/a).
        /// </remarks>
        private void ScrollByVelocity(float v)
        {
            float durationMultiplicator = 1800F / decelleration;
            float offsetMultiplicator = durationMultiplicator * 0.65f;

            int sign = v > 0 ? 1 : -1;

            int to = YOffset + sign * (int)(offsetMultiplicator * v * v);
            int duration = (int)Math.Abs(durationMultiplicator * v);
            EasingFunctionBase ease = GetEase(duration);

            IntTransition transition = new IntTransition
            {
                Tracking = TrackMode.Keep,
                Duration = duration,
                To = to,
                EasingFunction = ease
            };

            transition.Completed += (sender, arg) =>
            {
                FlickMode = false;
                IsScrolling = false;
            };

            FlickMode = true;
            IsScrolling = true;
            ScrollViewer.YOffsetProperty.BeginAnimation(this, transition);
        }

        private EasingFunctionBase GetEase(int duration)
        {
            switch (physicsMode)
            {
                case PhysicsMode.Decelleration: return PowerEase.QuadraticOut;
                case PhysicsMode.Friction: return new ExponentialEase { Exponent = Friction * duration };
                default: throw new NotSupportedException();
            }
        }


        /// <summary>
        /// Occurs when the display is being touched.
        /// </summary>
        /// <param name="e">TouchEventArgs.</param>
        protected internal override void OnTouchDown(TouchEventArgs e)
        {
            FlickMode = false;
            YOffsetProperty.StopAnimation(this);
            base.OnTouchDown(e);
        }

        /// <summary>
        /// Occurs when the control is panned.
        /// </summary>
        /// <param name="e">If e.AllowCancel is set to true, e.Cancel would abort to raise further OnPan methods.
        /// e.Value is the current value to be set.</param>
        protected internal override void OnPan(PanEventArgs e)
        {
            if (!e.Handled)
            {
                YOffset = e.Value.Y;
            }
        }

        /// <summary>
        /// Ensures that the given rectangle is visible inside this control.
        /// </summary>
        /// <param name="bounds">The bounds to be visible.</param>
        public override void BringIntoView(Rectangle bounds, bool animated)
        {
            ////ensure this control to be visible: (note: it's assumed that there is no scroll viewer inside a scroll viewer!)
            //BringIntoView(animated);

            // !! StopAnimation is important and eliminates jumping:
            YOffsetProperty.StopAnimation(this);
            int yOffset = YOffset;
            int top = bounds.Top;
            int bottom = bounds.Bottom;
            int min = yOffset;

            Rectangle visibleWindowBounds = VisibleBounds;
            int max = visibleWindowBounds.Height;

            if (top < min)
            {
                if (animated) GoToYOffset(top); else YOffset = top;
            }
            else if (bottom > (max + min))
            {
                int y = bottom - max;
                if (y != yOffset)
                {
                    if (animated) GoToYOffset(y); else YOffset = y;
                }
            }
        }

        public override void EnsureRestState(bool animated)
        {
            EnsureRestState(animated, ExponentialEase.In);
        }

        public virtual void EnsureRestState(bool animated, IEasingFunction ease)
        {
            //int yOffset = YOffsetProperty.GetTargetValue(this);
            int yOffset = YOffset;
            if (yOffset < 0)
            {
                FlickMode = false;
                if (animated)
                {
                    BounceToYOffset(0, ease);
                }
                else
                {
                    //      IsScrolling = false;  //note: don't set this to false, it would cause a flicker in some cases.
                    // note: when IsScrollbarVisible changes to false, ENsureRestState is called which restarts an animation, therefore IsScrollbarVisible is set to false before:
                    IsScrollbarVisible = false;
                    YOffsetProperty.StopAnimation(this);
                    YOffset = 0;
                }
            }
            else
            {
                int h = GetMaxYOffset(false);
                if (yOffset > h)
                {
                    FlickMode = false;
                    if (animated)
                    {
                        BounceToYOffset(h, ease);
                    }
                    else
                    {
                        //      IsScrolling = false;  //note: don't set this to false, it would cause a flicker in some cases.
                        // note: when IsScrollbarVisible changes to false, ENsureRestState is called which restarts an animation, therefore IsScrollbarVisible is set to false before:
                        IsScrollbarVisible = false;
                        YOffsetProperty.StopAnimation(this);
                        YOffset = h;
                    }
                }
            }
        }

        protected void BounceToYOffset(int offset, IEasingFunction ease)
        {
            IntTransition transition = new IntTransition
            {
                Duration = BounceDuration,
                EasingFunction = ease,
                To = offset,
            };
            transition.Completed += (s, e) =>
                {
                    IsScrolling = false;
                };
            IsScrolling = true;
            YOffsetProperty.Animate(this, transition);
        }

        protected void GoToYOffset(int offset)
        {
            int duration = Math.Min(GotoDuration, 4 * Math.Abs(offset - yOffset));
            IntTransition transition = new IntTransition
            {
                Duration = duration,
                EasingFunction = PowerEase.QuadraticOut,
                To = offset,
            };
            transition.Completed += (s, e) =>
                {
                    IsScrolling = false;
                };
            IsScrolling = true;
            YOffsetProperty.Animate(this, transition);
        }

        protected virtual int GetMaxYOffset(bool overlap)
        {
            int h = overlap ? Height : VisibleBounds.Height;

            return Math.Max(0, VirtualHeight - h);
        }

        protected override void OnSizeChanged()
        {
            if (Height > VirtualHeight)
            {
                EnsureRestState(IsInitialized && Visible);
            }
            base.OnSizeChanged();
        }


        protected virtual void OnYOffsetChanged(int oldValue)
        {
            Invalidate(InvalidationFlag.Layout);
            Window w = Window;
            if (w != null) w.RaiseViewChanged(this);

            if (FlickMode && IsOutOfBounds(yOffset, oldValue))
            {
                YOffsetProperty.StopAnimation(this);
            }
        }

        private bool IsOutOfBounds(int newValue, int oldValue)
        {
            int threshold = newValue - oldValue;
            if (newValue < threshold) return true;

            int maxThreshold = GetMaxYOffset(false) + threshold;
            return (newValue > maxThreshold);
        }


        #endregion
        #region accessors

        private static readonly PropertyAccessor<ScrollViewer, int> ScrollbarOpacityProperty = PropertyAccessor.Register<ScrollViewer, int>("ScrollbarOpacity", e => (e).ScrollbarOpacity, (e, v) => (e).ScrollbarOpacity = v);
        public static readonly PropertyAccessor<ScrollViewer, int> YOffsetProperty = PropertyAccessor.Register<ScrollViewer, int>("YOffset", e => (e).YOffset, (e, v) => (e).YOffset = v);

        #endregion
        #region events

        public static SparseEvent ScrollingChangedEvent = new SparseEvent();

        /// <summary>
        /// Occurs when the IsScrolling property changes.
        /// </summary>
        public event EventHandler ScrollingChanged
        {
            add { AddHandler(ScrollingChangedEvent, value); }
            remove { RemoveHandler(ScrollingChangedEvent, value); }
        }

        #endregion

    }

}
